 /*******************************************************************************
  * Copyright (c) 2006, 2007 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM - Initial API and implementation
  *******************************************************************************/
 package org.eclipse.update.internal.jarprocessor;

 import java.io.*;
 import java.util.*;
 import java.util.jar.*;

 /**
  * @author aniefer@ca.ibm.com
  *
  */
 public class JarProcessor {
     private List steps = new ArrayList();
     private String workingDirectory = ""; //$NON-NLS-1$
 private int depth = -1;
     private boolean verbose = false;
     private boolean processAll = false;
     private LinkedList containingInfs = new LinkedList();

     static public JarProcessor getUnpackProcessor(Properties properties) {
         if (!canPerformUnpack())
             throw new UnsupportedOperationException ();
         JarProcessor processor = new JarProcessor();
         processor.addProcessStep(new UnpackStep(properties));
         return processor;
     }

     static public JarProcessor getPackProcessor(Properties properties) {
         if (!canPerformPack())
             throw new UnsupportedOperationException ();
         JarProcessor processor = new JarProcessor();
         processor.addProcessStep(new PackStep(properties));
         return processor;
     }

     static public boolean canPerformPack() {
         return PackStep.canPack();
     }

     static public boolean canPerformUnpack() {
         return UnpackStep.canUnpack();
     }

     public String getWorkingDirectory() {
         return workingDirectory;
     }

     public void setWorkingDirectory(String dir) {
         if(dir != null)
             workingDirectory = dir;
     }
     
     public void setVerbose(boolean verbose) {
         this.verbose = verbose;
     }
     
     public void setProcessAll(boolean all){
         this.processAll = all;
     }

     public void addProcessStep(IProcessStep step) {
         steps.add(step);
     }

     public void clearProcessSteps() {
         steps.clear();
     }

     /**
      * Recreate a jar file. The replacements map specifies entry names to be replaced, the replacements are
      * expected to be found in directory.
      *
      * @param jar - The input jar
      * @param outputJar - the output
      * @param replacements - map of entryName -> new entryName
      * @param directory - location to find file for new entryName
      * @throws IOException
      */
     private void recreateJar(JarFile jar, JarOutputStream outputJar, Map replacements, File directory, Properties inf) throws IOException {
         InputStream in = null;
         boolean marked = false;
         try {
             Enumeration entries = jar.entries();
             for (JarEntry entry = (JarEntry) entries.nextElement(); entry != null; entry = entries.hasMoreElements() ? (JarEntry) entries.nextElement() : null) {
                 File replacement = null;
                 JarEntry newEntry = null;
                 if (replacements.containsKey(entry.getName())) {
                     String name = (String ) replacements.get(entry.getName());
                     replacement = new File(directory, name);
                     if (name != null) {
                         if (replacement.exists()) {
                             try {
                                 in = new BufferedInputStream(new FileInputStream(replacement));
                                 newEntry = new JarEntry(name);
                             } catch (Exception e) {
                                 if (verbose) {
                                     e.printStackTrace();
                                     System.out.println("Warning: Problem reading " +replacement.getPath() + ", using " + jar.getName() + File.separator + entry.getName() + " instead.");
                                 }
                             }
                         } else if (verbose) {
                             System.out.println("Warning: " + replacement.getPath() + " not found, using " + jar.getName() + File.separator + entry.getName() + " instead.");
                         }
                     }
                 }
                 if (newEntry == null) {
                     try {
                         in = new BufferedInputStream(jar.getInputStream(entry));
                         newEntry = new JarEntry(entry.getName());
                     } catch( Exception e ) {
                         if(verbose) {
                             e.printStackTrace();
                             System.out.println("ERROR: problem reading " + entry.getName() + " from " + jar.getName());
                         }
                         continue;
                     }
                 }
                 newEntry.setTime(entry.getTime());
                 outputJar.putNextEntry(newEntry);
                 if (entry.getName().equals(Utils.MARK_FILE_NAME)) {
                     //The eclipse.inf file was read in earlier, don't need to reread it, just write it out now
 Utils.storeProperties(inf, outputJar);
                     marked = true;
                 } else {
                     Utils.transferStreams(in, outputJar, false);
                 }
                 outputJar.closeEntry();
                 in.close();

                 //delete the nested jar file
 if (replacement != null) {
                     replacement.delete();
                 }
             }
             if (!marked) {
                 JarEntry entry = new JarEntry(Utils.MARK_FILE_NAME);
                 outputJar.putNextEntry(entry);
                 Utils.storeProperties(inf, outputJar);
                 outputJar.closeEntry();
             }
         } finally {
             Utils.close(outputJar);
             Utils.close(jar);
             Utils.close(in);
         }
     }

     private String recursionEffect(String entryName) {
         String result = null;
         for (Iterator iter = steps.iterator(); iter.hasNext();) {
             IProcessStep step = (IProcessStep) iter.next();

             result = step.recursionEffect(entryName);
             if (result != null)
                 entryName = result;
         }
         return result;
     }

     private void extractEntries(JarFile jar, File tempDir, Map data, Properties inf) throws IOException {
         if(inf != null ) {
             //skip if excluding children
 if(inf.containsKey(Utils.MARK_EXCLUDE_CHILDREN)){
                 String excludeChildren = inf.getProperty(Utils.MARK_EXCLUDE_CHILDREN);
                 if( Boolean.valueOf(excludeChildren).booleanValue() )
                     if(verbose){
                         for(int i = 0; i <= depth; i++)
                             System.out.print(" "); //$NON-NLS-1$
 System.out.println("Children of " + jar.getName() + "are excluded from processing.");
                     }
                     return;
             }
         }
         
         Enumeration entries = jar.entries();
         if (entries.hasMoreElements()) {
             for (JarEntry entry = (JarEntry) entries.nextElement(); entry != null; entry = entries.hasMoreElements() ? (JarEntry) entries.nextElement() : null) {
                 String name = entry.getName();
                 String newName = recursionEffect(name);
                 if (newName != null) {
                     if(verbose){
                         for(int i = 0; i <= depth; i++)
                             System.out.print(" "); //$NON-NLS-1$
 System.out.println("Processing nested file: " + name); //$NON-NLS-1$
 }
                     //extract entry to temp directory
 File extracted = new File(tempDir, name);
                     File parentDir = extracted.getParentFile();
                     if (!parentDir.exists())
                         parentDir.mkdirs();

                     InputStream in = null;
                     OutputStream out = null;
                     try {
                         in = jar.getInputStream(entry);
                         out = new BufferedOutputStream(new FileOutputStream(extracted));
                         Utils.transferStreams(in, out, true); //this will close both streams
 } finally {
                         Utils.close(in);
                         Utils.close(out);
                     }
                     extracted.setLastModified(entry.getTime());

                     //recurse
 containingInfs.addFirst(inf);
                     String dir = getWorkingDirectory();
                     setWorkingDirectory(parentDir.getCanonicalPath());
                     File result = processJar(extracted);

                     newName = name.substring(0, name.length() - extracted.getName().length()) + result.getName();
                     data.put(name, newName);

                     setWorkingDirectory(dir);
                     containingInfs.removeFirst();

                     //delete the extracted item leaving the recursion result
 if (!name.equals(newName))
                         extracted.delete();
                 }
             }
         }
     }

     private File preProcess(File input, File tempDir) {
         File result = null;
         for (Iterator iter = steps.iterator(); iter.hasNext();) {
             IProcessStep step = (IProcessStep) iter.next();
             result = step.preProcess(input, tempDir, containingInfs);
             if (result != null)
                 input = result;
         }
         return input;
     }

     private File postProcess(File input, File tempDir) {
         File result = null;
         for (Iterator iter = steps.iterator(); iter.hasNext();) {
             IProcessStep step = (IProcessStep) iter.next();
             result = step.postProcess(input, tempDir, containingInfs);
             if (result != null)
                 input = result;
         }
         return input;
     }

     private void adjustInf(File input, Properties inf) {
         for (Iterator iter = steps.iterator(); iter.hasNext();) {
             IProcessStep step = (IProcessStep) iter.next();
             step.adjustInf(input, inf, containingInfs);
         }
     }

     public File processJar(File input) throws IOException {
         ++depth;
         long lastModified = input.lastModified();
         File workingDir = new File(getWorkingDirectory());
         if (!workingDir.exists())
             workingDir.mkdirs();

         boolean skip = Utils.shouldSkipJar(input, processAll, verbose);
         if (depth == 0 && verbose) {
             if (skip)
                 System.out.println("Skipping " + input.getPath()); //$NON-NLS-1$
 else {
                 System.out.print("Running "); //$NON-NLS-1$
 for (Iterator iter = steps.iterator(); iter.hasNext();) {
                     IProcessStep step = (IProcessStep) iter.next();
                     System.out.print(step.getStepName() + " "); //$NON-NLS-1$
 }
                 System.out.println("on " + input.getPath()); //$NON-NLS-1$
 }
         }

         if (skip) {
             //This jar was not marked as conditioned, and we are only processing conditioned jars, so do nothing
 --depth;
             return input;
         }

         //pre
 File workingFile = preProcess(input, workingDir);

         //Extract entries from jar and recurse on them
 File tempDir = null;
         if (depth == 0) {
             tempDir = new File(workingDir, "temp." + workingFile.getName()); //$NON-NLS-1$
 } else {
             File parent = workingDir.getParentFile();
             tempDir = new File(parent, "temp_" + depth + '_' + workingFile.getName()); //$NON-NLS-1$
 }

         JarFile jar = new JarFile(workingFile, false);
         Map replacements = new HashMap();
         Properties inf = Utils.getEclipseInf(workingFile, verbose);
         extractEntries(jar, tempDir, replacements, inf);

         if (inf != null)
             adjustInf(workingFile, inf);

         //Recreate the jar with replacements.
 //TODO: This is not strictly necessary if we didn't change the inf file and didn't change any content
 File tempJar = null;
         tempJar = new File(tempDir, workingFile.getName());
         File parent = tempJar.getParentFile();
         if (!parent.exists())
             parent.mkdirs();
         JarOutputStream jarOut = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(tempJar)));
         recreateJar(jar, jarOut, replacements, tempDir, inf);

         jar.close();
         if (tempJar != null) {
             if (!workingFile.equals(input)) {
                 workingFile.delete();
             }
             workingFile = tempJar;
         }

         //post
 File result = postProcess(workingFile, workingDir);
         
         //have to normalize after the post steps
 normalize(result, workingDir);
         
         if (!result.equals(workingFile) && !workingFile.equals(input))
             workingFile.delete();
         if (!result.getParentFile().equals(workingDir)) {
             File finalFile = new File(workingDir, result.getName());
             if (finalFile.exists())
                 finalFile.delete();
             result.renameTo(finalFile);
             result = finalFile;
         }

         if (tempDir.exists())
             Utils.clear(tempDir);

         result.setLastModified(lastModified);
         --depth;
         return result;
     }
     
     private void normalize(File input, File workingDirectory) {
         if(input.getName().endsWith(Utils.PACKED_SUFFIX)) {
             //not a jar
 return;
         }
         try {
             File tempJar = new File(workingDirectory, "temp_" + input.getName()); //$NON-NLS-1$
 JarFile jar = null;
             try {
                 jar = new JarFile(input, false);
             } catch (JarException e) {
                 //not a jar
 return ;
             }
             JarOutputStream jarOut = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(tempJar)));
             InputStream in = null;
             try {
                 Enumeration entries = jar.entries();
                 for (JarEntry entry = (JarEntry) entries.nextElement(); entry != null; entry = entries.hasMoreElements() ? (JarEntry) entries.nextElement() : null) {
                     JarEntry newEntry = new JarEntry(entry.getName());
                     newEntry.setTime(entry.getTime());
                     in = new BufferedInputStream(jar.getInputStream(entry));
                     jarOut.putNextEntry(newEntry);
                     Utils.transferStreams(in, jarOut, false);
                     jarOut.closeEntry();
                     in.close();
                 }
             } finally {
                 Utils.close(jarOut);
                 Utils.close(jar);
                 Utils.close(in);
             }
             tempJar.setLastModified(input.lastModified());
             input.delete();
             tempJar.renameTo(input);
         } catch (IOException e) {
             if (verbose) {
                 System.out.println("Error normalizing jar " + input.getName()); //$NON-NLS-1$
 e.printStackTrace();
             }
         }
     }
 }

